<template>
  <div class="tab-content" ref="chart"></div>
</template>
<script lang="tsx"
  setup>
  import { Rect, Text } from '@antv/g';
  import {
    // Text,
    Badge,
    BaseBehavior,
    BaseNode,
    BaseTransform,
    CommonEvent,
    CubicHorizontal,
    ExtensionCategory,
    Graph,
    GraphEvent,
    iconfont,
    idOf,
    NodeData,
    positionOf,
    register,
    treeToGraphData,
  } from '@antv/g6';
  import { ref, onUnmounted } from 'vue';

  const style = document.createElement('style');
  style.innerHTML = `@import url('${iconfont.css}');`;
  document.head.appendChild(style);

  const chart = ref()
  let graphRef: Graph | null

  onUnmounted(() => {
    graphRef?.destroy()
  })
  const COLORS = [
    '#1783FF',
    '#00C9C9',
    '#F08F56',
    '#D580FF',
    '#7863FF',
    '#DB9D0D',
    '#60C42D',
    '#FF80CA',
    '#2491B3',
    '#17C76F',
  ];

  const RootNodeStyle = {
    fill: '#EFF0F0',
    labelFill: '#262626',
    labelFontSize: 16,
    labelFontWeight: 600,
    labelPlacement: 'center',
    ports: [{ placement: 'right' }, { placement: 'left' }],
    radius: 4,
  };

  const NodeStyle = {
    fill: 'transparent',
    labelPlacement: 'center',
    labelFontSize: 12,
    ports: [{ placement: 'right-bottom' }, { placement: 'left-bottom' }],
  };

  const TreeEvent = {
    COLLAPSE_EXPAND: 'collapse-expand',
  };

  let textShape;
  const measureText = (text) => {
    if (!textShape) textShape = new Text({ style: text });
    textShape.attr(text);
    return textShape.getBBox().width;
  };

  const getNodeWidth = (text, isRoot) => {
    return isRoot
      ? measureText({ text, fontSize: RootNodeStyle.labelFontSize }) + 20
      : measureText({ text, fontSize: NodeStyle.labelFontSize }) + 30;
  };

  class MindmapNode extends BaseNode {
    static defaultStyleProps = {
      showIcon: false,
    };

    constructor(options) {
      Object.assign(options.style, MindmapNode.defaultStyleProps);
      super(options);
    }

    get childrenData() {
      return this.context.model.getChildrenData(this.id);
    }

    get getNodeData() {
      return this.context.model.getNodeData([this.id])[0];
    }

    get rootId() {
      return idOf(this.context.model.getRootsData()[0]);
    }

    isShowCollapse(attributes) {
      const { collapsed, showIcon } = attributes;
      // return !collapsed && showIcon && this.childrenData.length > 0;
      return !collapsed && this.childrenData.length > 0;
    }

    getCollapseStyle(attributes) {
      const { showIcon, color, direction } = attributes;
      if (!this.isShowCollapse(attributes)) return false;
      const [width, height] = this.getSize(attributes);

      return {
        backgroundFill: color,
        backgroundHeight: 12,
        backgroundWidth: 12,
        cursor: 'pointer',
        fill: '#fff',
        fontFamily: 'iconfont',
        fontSize: 8,
        text: '\ue6e4',
        textAlign: 'center',
        transform: direction === 'left' ? 'rotate(90deg)' : 'rotate(-90deg)',
        // visibility: showIcon ? 'visible' : 'hidden',
        x: direction === 'left' ? -6 : width + 6,
        y: height,
        zIndex: 5,
      };
    }

    drawCollapseShape(attributes, container) {
      const iconStyle = this.getCollapseStyle(attributes);
      const btn = this.upsert('collapse-expand', Badge, iconStyle, container);

      this.forwardEvent(btn, CommonEvent.CLICK, (event) => {
        event.stopPropagation();
        this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {
          id: this.id,
          collapsed: !attributes.collapsed,
        });
      });
    }

    getCountStyle(attributes) {
      const { collapsed, color, direction } = attributes;
      const count = this.context.model.getDescendantsData(this.id).length;
      if (!collapsed || count === 0) return false;
      const [width, height] = this.getSize(attributes);
      return {
        backgroundFill: color,
        backgroundHeight: 12,
        backgroundWidth: 12,
        cursor: 'pointer',
        fill: '#fff',
        fontSize: 8,
        text: count.toString(),
        textAlign: 'center',
        x: direction === 'left' ? -6 : width + 6,
        y: height,
        zIndex: 6,
      };
    }

    drawCountShape(attributes, container) {
      const countStyle = this.getCountStyle(attributes);
      const btn = this.upsert('count', Badge, countStyle, container);

      this.forwardEvent(btn, CommonEvent.CLICK, (event) => {
        event.stopPropagation();
        this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {
          id: this.id,
          collapsed: false,
        });
      });
    }

    forwardEvent(target, type, listener) {
      if (target && !Reflect.has(target, '__bind__')) {
        Reflect.set(target, '__bind__', true);
        target.addEventListener(type, listener);
      }
    }

    getKeyStyle(attributes) {
      const [width, height] = this.getSize(attributes);
      const keyShape = super.getKeyStyle(attributes);
      return { width, height, ...keyShape };
    }

    drawKeyShape(attributes, container) {
      const keyStyle = this.getKeyStyle(attributes);
      return this.upsert('key', Rect, keyStyle, container);
    }

    render(attributes = this.parsedAttributes, container = this) {
      super.render(attributes, container);

      this.drawCollapseShape(attributes, container);
      this.drawCountShape(attributes, container);
    }
  }

  class MindmapEdge extends CubicHorizontal {
    get rootId() {
      return idOf(this.context.model.getRootsData()[0]);
    }

    getKeyPath(attributes) {
      const path = super.getKeyPath(attributes);
      const isRoot = this.targetNode.id === this.rootId;
      const labelWidth = getNodeWidth(this.targetNode.getNodeData.labelText, isRoot);

      const [, tp] = this.getEndpoints(attributes);
      const sign = this.sourceNode.getCenter()[0] < this.targetNode.getCenter()[0] ? 1 : -1;
      return [...path, ['L', tp[0] + labelWidth * sign, tp[1]]];
    }
  }

  class CollapseExpandTree extends BaseBehavior {
    constructor(context, options) {
      super(context, options);
      this.bindEvents();
    }

    update(options) {
      this.unbindEvents();
      super.update(options);
      this.bindEvents();
    }

    bindEvents() {
      const { graph } = this.context;

      // graph.on(NodeEvent.POINTER_ENTER, this.showIcon);
      // graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon);
      graph.on(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);

    }

    unbindEvents() {
      const { graph } = this.context;
      // graph.off(NodeEvent.POINTER_ENTER, this.showIcon);
      // graph.off(NodeEvent.POINTER_LEAVE, this.hideIcon);
      graph.off(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);
    }

    status = 'idle';

    showIcon = (event) => {
      this.setIcon(event, true);
    };

    hideIcon = (event) => {
      this.setIcon(event, false);
    };

    setIcon = (event, show) => {
      if (this.status !== 'idle') return;
      const { target } = event;
      const id = target.id;
      const { graph, element } = this.context;
      graph.updateNodeData([{ id, style: { showIcon: show } }]);
      element.draw({ animation: false, silence: true });
    };

    onCollapseExpand = async (event) => {
      this.status = 'busy';
      const { id, collapsed } = event;
      const { graph } = this.context;
      await graph.frontElement(id);
      if (collapsed) {
        await graph.collapseElement(id)
      } else {
        await graph.expandElement(id)
      };
      await graph.focusElement(id)
      this.status = 'idle';
    };

  }

  class AssignElementColor extends BaseTransform {
    beforeDraw(data) {
      const { nodes = [], edges = [] } = this.context.graph.getData();

      const nodeColorMap = new Map();

      let colorIndex = 0;
      const dfs = (nodeId, color) => {
        const node = nodes.find((datum) => datum.id == nodeId);
        if (!node) return;

        if (node.data?.depth !== 0) {
          // const nodeColor = color || COLORS[colorIndex++ % COLORS.length];

          const nodeColor = node.ratio === 0 ? '#999' : COLORS[colorIndex++ % COLORS.length];
          node.style ||= {};
          node.style.color = nodeColor;
          nodeColorMap.set(nodeId, nodeColor);
        }

        node.children?.forEach((childId) => dfs(childId, node.style.color));
      };

      nodes.filter((node) => node.data?.depth === 0).forEach((rootNode) => dfs(rootNode.id));

      edges.forEach((edge) => {
        edge.style ||= {};
        edge.style.stroke = nodeColorMap.get(edge.target);
      });

      return data;
    }
  }

  register(ExtensionCategory.NODE, 'mindmap', MindmapNode);
  register(ExtensionCategory.EDGE, 'mindmap', MindmapEdge);
  register(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree);
  register(ExtensionCategory.TRANSFORM, 'assign-element-color', AssignElementColor);

  const getNodeSide = (nodeData, parentData) => {
    if (!parentData) return 'center';

    const nodePositionX = positionOf(nodeData)[0];
    const parentPositionX = positionOf(parentData)[0];
    return parentPositionX > nodePositionX ? 'left' : 'right';
  };

  const formatData = (data) => {
    return treeToGraphData(data, {
      getNodeData: (datum, depth) => {
        const ratio = typeof (datum.ratio) == 'number' ? `${(datum.ratio * 100).toFixed(2)}%` : '-'
        const labelText = depth === 0 ? `${datum.classifyName}：${datum.total || '-'}` : `${datum.classifyName}：${datum.total}（${ratio}）`
        datum.data ||= {};
        datum.data.depth = depth;
        datum.id = datum.treeId
        datum.labelText = labelText
        if (!datum.children) return datum as NodeData;
        return { ...datum, children: datum.children.map((child) => child.treeId) };
      },

      getEdgeData: (source, target) => ({
        source: source.treeId,
        target: target.treeId
      }),
    })
  }
  const initGraph = async (data) => {
    if (graphRef) {
      await graphRef?.destroy()
      graphRef = null
    }
    const errorData = !data || data.length === 0
    if (errorData) return
    const graphH = chart.value.clientHeight
    if (graphH === 0) {
      setTimeout(() => {
        initGraph(data)
      }, 0)
      return
    }

    const rootId = data.treeId;
    graphRef = new Graph({
      container: chart.value,
      autoFit: 'view',
      data: formatData(data),
      node: {
        type: 'mindmap',
        style: function (d) {
          const direction = getNodeSide(d, this.getParentData(idOf(d), 'tree'));
          const isRoot = idOf(d) === rootId;
          const width = getNodeWidth(d.labelText, isRoot)
          return {
            direction,
            labelText: d.labelText,
            size: [width, 30],
            // 通过设置节点标签背景来扩大节点的交互区域
            labelBackground: true,
            labelBackgroundFill: 'transparent',
            labelPadding: direction === 'left' ? [2, 0, 10, 20] : [2, 40, 10, 0],
            ...(isRoot ? RootNodeStyle : NodeStyle),
          };
        },
      },
      edge: {
        type: 'mindmap',
        style: { lineWidth: 2 },
      },
      layout: {
        type: 'mindmap',

        // direction: 'H',
        direction: 'LR',
        getHeight: () => 30,
        getWidth: (node) => {
          return getNodeWidth(node.labelText, node.id === rootId)
        },
        getVGap: () => 6,
        getHGap: () => 60,
        animation: false,
      },
      zoomRange: [0.1, 2],

      behaviors: ['drag-canvas', 'zoom-canvas', 'collapse-expand-tree'],
      transforms: ['assign-element-color'],
      plugins: [{
        type: 'minimap',
        size: [150, 150],
        position: 'right-bottom',
      }, {
        type: 'fullscreen',
        trigger: {
          // exit: ['Esc']
        }
      },
      {
        type: 'toolbar',
        position: 'top-right',
        onClick: (item) => {
          if (item === 'request-fullscreen') {
            const full = graph.getPluginInstance('fullscreen')
            full.request()
          }
        },
        getItems: () => {
          return [
            { id: 'request-fullscreen', value: 'request-fullscreen' },
          ]
        },
      },
      ],
      animation: false,
    });

    graphRef.once(GraphEvent.AFTER_RENDER, () => {
      graphRef!.fitView();
    });
    await graphRef.render();
    initCollapseData([data])
  }
  // 初始化折叠起为0的数据
  const initCollapseData = (data) => {
    const collapseNodesWithZeroRatio = (data) => {
      data.forEach((node) => {
        if (node.ratio === 0) {
          graphRef!.emit(TreeEvent.COLLAPSE_EXPAND, {
            id: node.treeId,
            collapsed: true, // 设置为 true 以折叠节点
          });
          return;
        }
        if (node.children) {
          collapseNodesWithZeroRatio(node.children);
        }
      });
    };
    collapseNodesWithZeroRatio(data);
    graphRef!.once(GraphEvent.AFTER_RENDER, () => {
      graphRef!.fitView();
    });
    graphRef!.render();
  }

  defineExpose({
    initGraph
  })
</script>